Skip to content

fix(mapper): unwrap reflective spec construction exceptions in toSpec - #189

Merged
shihyuho merged 1 commit into
jakartafrom
fix/177-unwrap-reflective-spec-exception
Jul 16, 2026
Merged

fix(mapper): unwrap reflective spec construction exceptions in toSpec#189
shihyuho merged 1 commit into
jakartafrom
fix/177-unwrap-reflective-spec-exception

Conversation

@shihyuho

Copy link
Copy Markdown
Member

Summary

SimpleSpecification.newSpec built specs via Constructor.newInstance(...) under @SneakyThrows. Constructor.newInstance wraps any exception thrown inside a spec constructor in a java.lang.reflect.InvocationTargetException, and @SneakyThrows rethrew that wrapper verbatim. As a result, the constructor-time validation exceptions — TypeMismatchException (from In / Between / boolean & comparable specs) and IllegalArgumentException (from Between) — were masked as InvocationTargetException at the SpecMapper.toSpec boundary. A downstream handler keyed on TypeMismatchException (e.g. to map it to an HTTP 400) never fired; callers got an opaque failure with the real message buried in the cause.

This change catches InvocationTargetException in newSpec and surfaces the original cause: RuntimeException / Error causes are rethrown unchanged (preserving their message), and checked causes are wrapped in IllegalStateException rather than leaked raw. InvocationTargetException can no longer escape newSpec.

Closes #177

Acceptance criteria

  • SpecMapper.toSpec on a POJO whose @Spec(In.class) field holds a non-Iterable value throws TypeMismatchException (not InvocationTargetException) with the same message the direct constructor produces.
  • SpecMapper.toSpec on a POJO whose @Spec(Between.class) field is bound to a 1-element list throws IllegalArgumentException with the constructor's message (@Between expected exact 2 elements...).
  • InvocationTargetException never propagates out of SimpleSpecification.newSpec; a RuntimeException cause is rethrown unchanged, a checked cause is wrapped (IllegalStateException), not leaked raw.
  • New tests exercise the SpecMapper.toSpec path (constructing a query POJO and mapping it), not the direct spec constructor, for the In/non-Iterable and Between/1-element cases, asserting the surfaced exception type and message.
  • All existing direct-construction typeMismatch tests remain green.
  • mvn test passes.

Out of scope (per brief): MAINT-04 (CTX_JOIN hidden-invariant) is deferred pending a maintainer design decision; spec constructor exception types and any starter/ HTTP-layer handling are unchanged.

Testing

mvn test across all modules — BUILD SUCCESS. New coverage: InTest.typeMismatchThroughMapper (In on a String field → TypeMismatchException) and BetweenTest.oneElementThroughMapper (Between bound to a 1-element list → IllegalArgumentException), both driving the real SpecMapper.toSpec path.

SimpleSpecification.newSpec built specs via Constructor.newInstance under
@SneakyThrows, which rethrew the InvocationTargetException wrapper verbatim.
Constructor-time validation throws (TypeMismatchException from In/Between and
IllegalArgumentException from Between) were therefore masked as
InvocationTargetException at the SpecMapper.toSpec boundary, so downstream
handlers keyed on TypeMismatchException never fired.

Catch InvocationTargetException in newSpec and surface the original cause:
RuntimeException/Error causes are rethrown unchanged, checked causes are
wrapped in IllegalStateException. Add tests that exercise the mapper path
(POJO -> SpecMapper.toSpec) for the In/non-Iterable and Between/1-element
cases.

Closes #177

Co-authored-by: Claude Opus 4.8 <[email protected]>

@shihyuho shihyuho left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE (advisory) — reviewed all 9 axes, no must-fix

Non-blocking (2)

  • FYI · Spec conformance — Closes #177 will auto-close an issue whose third finding (MAINT-04, the CTX_JOIN hidden invariant) this PR deliberately does not address, silently dropping it from tracking.
  • Nit · Test coverage — mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/SimpleSpecification.java:81 — the checked-cause wrapping branch (and the Error branch) ships with no test, so a regression that re-leaks a raw InvocationTargetException for a checked cause would keep the suite green despite that behaviour being a stated acceptance criterion.
Coverage & checks
Axis Status
Correctness clean — verified the catch is legal and reachable (In/Between throw from inside the constructor, so InvocationTargetException is the wrapper), that getCause() is the right accessor, and that the cause can never be null
Spec conformance 1 finding
Scope clean — every added line traces to COR-09/TEST-07 of the linked issue; both test files read in full at the head SHA, no drive-by edits
Convention clean — read the head commit message: a well-formed conventional commit (69 chars, under the 100-char header limit); CONTRIBUTING.md's "cover your changes with tests" rule is satisfied
Security clean — domainClass comes from the compile-time @Spec annotation literal, not request data; the newInstance call is byte-identical to the deleted lines
Readability clean — flat catch with two instanceof pattern guards, no nesting; the comment explains why rather than restating what; @SneakyThrows confirmed still load-bearing
Architecture clean — newSpec confirmed repo-wide as the single reflective-construction site, so this is the correct chokepoint with no sibling site left un-unwrapped
Performance clean — a try block that completes normally is zero-cost on the JVM, so the success path is unchanged; the catch is O(1) and allocation-free on the rethrow branches
Test coverage 1 finding
  • Verified — all 4 checks green: commits, pr-title, continuous-integration/jenkins/branch, continuous-integration/jenkins/pr-merge (read via gh pr checks, never run locally)
  • Out-of-band — grepped the whole repo for newInstance/InvocationTargetException to confirm newSpec is the only reflective-construction site · read In, Between and the head commit message at the pinned SHA

🤖 Reviewed by Claude Opus 4.8

(self-review — same account; GitHub records this as event=COMMENT with no badge, so the verdict above is advisory.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[WP5] Reflective spec construction masks TypeMismatchException + hidden context invariant

1 participant